﻿using PerBlue.Grunt;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Timers;
using System.Threading;
using System.Resources;
using System.Reflection;
using Tempest.Messages;
using System.Linq;
using System.Text.RegularExpressions;

namespace TempestChessComputer
{
    class TempestComputerPlayer
    {
        GruntTcpConnection connection;        
        int PORT = 6112;
        string HOST = "server.TempestChess.com";
        int playerId, sessionId, tableId;        
        string helloMessage = "Hello!";

        //set computer name/pw
        string userName = "TestBot3000(c)";
        string password = "testbot3000";
        string bot_owner = "devilant"; //case sensitive

        List<string> whiteList = new List<string>();

        //command line arguments
        Dictionary<string, Action<string>> commands = new Dictionary<string,Action<string>>();
        Dictionary<string, Action<string>> adminCommands = new Dictionary<string,Action<string>>();

        //Lobby ID -> (list of player IDs at lobby, LobbySettings)
        Dictionary<int, Tuple<List<int>, LobbySettings>> gameList = new Dictionary<int, Tuple<List<int>, LobbySettings>>();

        //player name -> ID
        Dictionary<string, int> playerList = new Dictionary<string, int>();
        
        //Set the game type/rated settings
        GameType gameType = GameType.STANDARD;
        bool rated = false;
        int teamConfiguration = 0;    //set to 1 for team fast four way
        int delayDuration;
        int moveSpeed;

        Game theGame;
        int playerPosition;

        static void Main(string[] args)
        {
            System.Console.WriteLine("Starting Tempest Computer Player!!!");
            TempestComputerPlayer computer = new TempestComputerPlayer();
            computer.connectToServer();

            Console.Read();
        }

        public TempestComputerPlayer()
        {
            commands["create"] = (s) => CreateTable(s);
            commands["join"] = (s) => JoinTable(s);
            commands["leave"] = (s) => SendLeaveLobby();

            commands["sit"] = (s) => SendSit(s);
            commands["stand"] = (s) => SendStandRequest();

            commands["draw"] = (s) => SendOfferDraw();
            commands["resign"] = (s) => SendResign();
            
            commands["gamelist"] = (s) => PrintGameList();

            adminCommands["whitelist"] = (s) => AddToWhiteList(s);
            adminCommands["blacklist"] = (s) => RemoveFromWhiteList(s);

            whiteList.Add(bot_owner);
        }

        private void JoinTable(string playerName) {
            Console.WriteLine("Finding {0}", playerName);

            if (!playerList.ContainsKey(playerName))
            {
                Console.WriteLine("Couldn't find {0}", playerName);
                return;
            }

            int playerID = playerList[playerName];

            foreach(KeyValuePair<int, Tuple<List<int>, LobbySettings> > ls in gameList) {
                if (ls.Value.Item1.Contains(playerID))
                {
                    Console.WriteLine("Joining Lobby {0}", ls.Key);
                    SendJoinLobby(ls.Key);
                    return;
                }
            }

            Console.WriteLine("Couldn't find lobby for player ID {0}", playerID);
        }

        private void SendSit(string seat)
        {
            Console.WriteLine("Trying to sit in seat {0}", int.Parse(seat));
            SendSitRequest(int.Parse(seat));
        }

        private void PrintGameList()
        {
            foreach (KeyValuePair<int, Tuple<List<int>, LobbySettings> > il in gameList)
            {
                Console.WriteLine("At lobby {0}", il.Key);
                Console.WriteLine("Gametype: " + il.Value.Item2.GameType);
                foreach (int ID in il.Value.Item1)
                {
                    try
                    {
                        KeyValuePair<string, int> player = playerList.First(p => p.Value == ID);
                        Console.WriteLine(player.Key);
                    }
                    catch (InvalidOperationException e) { }
                }
            }
        }

        private void AddToWhiteList(string name)
        {
            whiteList.Add(name);
            Console.WriteLine("{0} added to whitelist", name);
            Console.WriteLine("Whitelist now contains");
            foreach (string s in whiteList) Console.WriteLine(s);
        }

        private void RemoveFromWhiteList(string name)
        {
            whiteList.Remove(name);
            Console.WriteLine("{0} removed from whitelist", name);
            Console.WriteLine("Whitelist now contains:");
            foreach(string s in whiteList)  Console.WriteLine(s);
        }

        private void CreateTable(string gameName)
        {            
            if (gameName == "s") gameType = GameType.STANDARD;
            else if (gameName == "fs") gameType = GameType.FASTSTANDARD;
            else if (gameName == "bug") gameType = GameType.BUGHOUSE;
            else if (gameName == "crazy") gameType = GameType.CRAZYHOUSE;
            else if (gameName == "ffw") gameType = GameType.FASTFOURWAY;
            else if (gameName == "mad") gameType = GameType.FOURWAYCRAZYHOUSE;
            else
            {
                Console.WriteLine("Invalid option. Options are s, fs, bug, crazy, ffw, mad");
                Console.WriteLine("Defaulting to standard.");
                gameType = GameType.STANDARD;
            }
            CreateTable();
        }

        private void CreateTable()
        {
            switch (gameType)
            {
                case GameType.STANDARD:
                    moveSpeed = 1000;
                    delayDuration = 10000;
                    SendCreateLobby(GameType.STANDARD, moveSpeed, delayDuration, rated, teamConfiguration);                    
                    break;

                case GameType.FASTSTANDARD:
                    moveSpeed = 100;
                    delayDuration = 1000;
                    SendCreateLobby(GameType.FASTSTANDARD, moveSpeed, delayDuration, rated, teamConfiguration);                    
                    break;

                case GameType.BUGHOUSE:
                    moveSpeed = 500;
                    delayDuration = 5000;
                    SendCreateLobby(GameType.BUGHOUSE, moveSpeed, delayDuration, rated, teamConfiguration);                    
                    break;

                case GameType.CRAZYHOUSE:
                    moveSpeed = 800;
                    delayDuration = 8000;
                    SendCreateLobby(GameType.CRAZYHOUSE, moveSpeed, delayDuration, rated, teamConfiguration);                    
                    break;

                case GameType.FASTFOURWAY:
                    moveSpeed = 100;
                    delayDuration = 1000;
                    SendCreateLobby(GameType.FASTFOURWAY, moveSpeed, delayDuration, rated, teamConfiguration);                    
                    break;

                case GameType.FOURWAYCRAZYHOUSE:
                    moveSpeed = 300;
                    delayDuration = 3000;
                    SendCreateLobby(GameType.FOURWAYCRAZYHOUSE, moveSpeed, delayDuration, rated, teamConfiguration);                    
                    break;                    
            }            
        }       

        private void OnInvite(GruntConnection conn, Invite message)
        {
        }

        private void OnLobbySettings(GruntConnection conn, LobbySettings message)
        {
            Tuple<List<int>, LobbySettings> table = gameList[message.LobbyId];
            Tuple<List<int>, LobbySettings> updatedTable = new Tuple<List<int>, LobbySettings>(table.Item1, message);
            gameList[message.LobbyId] = updatedTable;
        }

        private void OnLobbyCreated(GruntConnection conn, LobbyCreated message)
        {
            gameList[message.Settings.LobbyId] = new Tuple<List<int>, LobbySettings>(new List<int>(), message.Settings);
        }

        private void OnPieceInHand(GruntConnection conn, PieceInHand message)
        {           
        }

        private void OnPlayerDisconnected(GruntConnection conn, PlayerDisconnected message)
        {           
        }

        private void OnPlayerRecord(GruntConnection conn, PlayerRecord message)
        {            
        }

        private void OnPrivateMessage(GruntConnection conn, PrivateMessage message)
        {
            Match m = Regex.Match(message.Message, @"([a-z]+)\s?(.*)", RegexOptions.IgnoreCase);

            if (!m.Success) Console.WriteLine("Recieved invalid private message.");

            //White-listed commands
            if (m.Success && whiteList.Contains(message.Player))
            {
                Console.WriteLine("Command received ({0}) Arguments: {1}", m.Groups[1].Value, m.Groups[2].Value);

                if (commands.ContainsKey(m.Groups[1].Value))
                    commands[m.Groups[1].Value].Invoke(m.Groups[2].Value);
            }

            //Admin commands
            if (m.Success && message.Player == bot_owner)
            {
                if (adminCommands.ContainsKey(m.Groups[1].Value))
                    adminCommands[m.Groups[1].Value].Invoke(m.Groups[2].Value);        
            }
                
        }

        private void OnDrawStatus(GruntConnection conn, DrawStatus message)
        {
        }

        private void OnPlayerReadyStatus(GruntConnection conn, PlayerReadyStatus message)
        {
            if (message.PlayerId == this.playerId)
            {
                Console.WriteLine("Computer is READY FOR GAME TO BEGIN");
            }
        }

        private void OnPlayerJoinedLobby(GruntConnection conn, PlayerJoinedLobby message)
        {
            if (!gameList.ContainsKey(message.LobbyId)) 
                gameList[message.LobbyId] = new Tuple<List<int>,LobbySettings>(new List<int>(), new LobbySettings());
            if(!gameList[message.LobbyId].Item1.Contains(message.PlayerId))
                gameList[message.LobbyId].Item1.Add(message.PlayerId);

            if (message.PlayerId == this.playerId)
            {
                Console.WriteLine("Computer joined table: " + message.LobbyId);
                this.tableId = message.LobbyId;
                this.gameType = gameList[message.LobbyId].Item2.GameType;
                this.teamConfiguration = gameList[message.LobbyId].Item2.TeamConfiguration;
                this.moveSpeed = gameList[message.LobbyId].Item2.MillisecondsPerSquare;
                this.delayDuration = gameList[message.LobbyId].Item2.MillisecondsForWait;
                Console.WriteLine("Sending seat request for seat 0");
                this.SendSitRequest(0);
            }
        }

        private void OnPlayerSitStand(GruntConnection conn, PlayerSit message)
        {
            if (message.PlayerId == this.playerId)
            {
                Console.WriteLine("Computer is seated!");
                playerPosition = message.Position;
                this.SendReadyRequest(true);                
            }
            if (message.LobbyId == this.tableId && message.IsSeated)
            {
                this.SendChatMessage(helloMessage);
            }
        }
        private void OnPlayerLeftLobby(GruntConnection conn, PlayerLeftLobby message)
        {
            gameList[message.LobbyId].Item1.Remove(message.PlayerId);
        }

        private void OnPlayersConnectedList(GruntConnection conn, PlayersConnectedList message)
        {           
            foreach (PlayerConnected p in message.Players) {
                playerList[p.Name] = p.PlayerId;
            }
        }
        private void OnPlayerConnected(GruntConnection conn, PlayerConnected message)
        {
            playerList[message.Name] = message.PlayerId;
        }

        private void OnPong(GruntConnection conn, Ping message)
        {            
        }

        private void OnLobbyStatus(GruntConnection conn, LobbyStatus message)
        {
            Tuple<List<int>, LobbySettings> table = gameList[message.LobbyId];
            table.Item1.Clear();
            table.Item1.AddRange(message.Observers);
            foreach (SeatedPlayerStatus s in message.Players)
            {
                gameList[message.LobbyId].Item1.Add(s.PlayerId);
            }                        
        }

        private void OnBoardStatus(GruntConnection conn, BoardStatus message)
        {
        }

        private void OnPiecePlaced(GruntConnection conn, PiecePlaced message)
        {                        
            theGame.piecePlaced(message);
        }

        private void OnPieceMoved(GruntConnection conn, PieceMoved message)
        {            
            theGame.pieceMoved(message);
        }

        private void OnPieceInDanger(GruntConnection conn, PieceInDanger message)
        {            
            theGame.pieceInDanger(message);
        }

        private void OnPieceCaptured(GruntConnection conn, PieceCaptured message)
        {
            theGame.pieceCaptured(message);
        }

        private void OnPlayerStatus(GruntConnection conn, PlayerStatus message)
        {            
        }

        private void OnChatMessage(GruntConnection conn, ChatMessage message)
        {
        }

        private void OnGameStarted(GruntConnection conn, GameStarted message)
        {
            System.Console.WriteLine("Game started");
            theGame = new Game(this, gameType, playerPosition, teamConfiguration, moveSpeed, delayDuration);            
            theGame.startGame(message.StartTime);
        }

        private void OnGameStatus(GruntConnection conn, GameStatus message)
        {            
            if (message.GameOver)
            {                
                theGame.stopGame();
                SendReadyRequest(true);
            }
        }

        private void OnBroadcast(GruntConnection conn, Broadcast message)
        {            
        }

        private byte[] doHash(string password)
        {
            SHA256Managed s = new SHA256Managed();
            ASCIIEncoding encoding = new ASCIIEncoding();
            return s.ComputeHash(encoding.GetBytes(password));
        }

        private void connectToServer()
        {
            InitializeGrunt();

            System.Console.WriteLine("Determining tempest server IP address");
            IPAddress address = null;
            foreach (IPAddress address2 in Dns.GetHostEntry(HOST).AddressList)
            {
                if (address2.AddressFamily == AddressFamily.InterNetwork)
                {
                    address = new IPAddress(address2.GetAddressBytes());
                }
            }
            if (address == null)
            {
                System.Console.WriteLine("Failed to get IP address");
                return;
            }
            System.Console.WriteLine("IP address found: " + address.ToString());

            System.Console.WriteLine("Starting connection");
            connection.Start();

            System.Console.WriteLine("Sending connection request to server");
            ConnectionRequest message = new ConnectionRequest();
            message.UserName = userName;
            message.NetworkVersion = 8;
            this.connection.Send(message);
        }

        private void OnConnectionResponse(GruntConnection conn, ConnectionResponse message)
        {
            if (message.Status == ConnectionStatusEnum.SUCCESS)
            {
                sessionId = message.SessionId;
                playerId = message.PlayerId;
                System.Console.WriteLine("Connected to lobby");
                System.Console.WriteLine("Player id: " + playerId + " Session id: " + sessionId);                
            }
            else
            {
                this.connection.Shutdown();
                this.connection = null;
            }

            if (message.Status == ConnectionStatusEnum.SUCCESS)
            {
                System.Console.WriteLine("Connection successful");
                CreateTable();
            }
            else
                System.Console.WriteLine("Connection failed");
        }
        
        private void InitializeGrunt()
        {
            this.connection = new GruntTcpConnection(HOST, PORT, MessageFactory.Instance, true);
            this.connection.RegisterCallback<ConnectionResponse>(new GruntCallback<ConnectionResponse>(this.OnConnectionResponse));
            this.connection.RegisterCallback<ConnectionAuthenticationDemand>(new GruntCallback<ConnectionAuthenticationDemand>(this.OnConnectionAuthenticationDemand));
            this.connection.RegisterCallback<PlayerConnected>(new GruntCallback<PlayerConnected>(this.OnPlayerConnected));
            this.connection.RegisterCallback<PlayerDisconnected>(new GruntCallback<PlayerDisconnected>(this.OnPlayerDisconnected));
            this.connection.RegisterCallback<PlayersConnectedList>(new GruntCallback<PlayersConnectedList>(this.OnPlayersConnectedList));
            this.connection.RegisterCallback<PlayerRecord>(new GruntCallback<PlayerRecord>(this.OnPlayerRecord));
            this.connection.RegisterCallback<LobbyStatus>(new GruntCallback<LobbyStatus>(this.OnLobbyStatus));
            this.connection.RegisterCallback<BoardStatus>(new GruntCallback<BoardStatus>(this.OnBoardStatus));
            this.connection.RegisterCallback<PiecePlaced>(new GruntCallback<PiecePlaced>(this.OnPiecePlaced));
            this.connection.RegisterCallback<PieceMoved>(new GruntCallback<PieceMoved>(this.OnPieceMoved));
            this.connection.RegisterCallback<PieceInDanger>(new GruntCallback<PieceInDanger>(this.OnPieceInDanger));
            this.connection.RegisterCallback<PieceCaptured>(new GruntCallback<PieceCaptured>(this.OnPieceCaptured));
            this.connection.RegisterCallback<PieceInHand>(new GruntCallback<PieceInHand>(this.OnPieceInHand));
            this.connection.RegisterCallback<LobbyCreated>(new GruntCallback<LobbyCreated>(this.OnLobbyCreated));
            this.connection.RegisterCallback<LobbySettings>(new GruntCallback<LobbySettings>(this.OnLobbySettings));
            this.connection.RegisterCallback<Invite>(new GruntCallback<Invite>(this.OnInvite));
            this.connection.RegisterCallback<PlayerJoinedLobby>(new GruntCallback<PlayerJoinedLobby>(this.OnPlayerJoinedLobby));
            this.connection.RegisterCallback<PlayerLeftLobby>(new GruntCallback<PlayerLeftLobby>(this.OnPlayerLeftLobby));
            this.connection.RegisterCallback<PlayerSit>(new GruntCallback<PlayerSit>(this.OnPlayerSitStand));
            this.connection.RegisterCallback<PlayerReadyStatus>(new GruntCallback<PlayerReadyStatus>(this.OnPlayerReadyStatus));
            this.connection.RegisterCallback<PlayerStatus>(new GruntCallback<PlayerStatus>(this.OnPlayerStatus));
            this.connection.RegisterCallback<ChatMessage>(new GruntCallback<ChatMessage>(this.OnChatMessage));
            this.connection.RegisterCallback<PrivateMessage>(new GruntCallback<PrivateMessage>(this.OnPrivateMessage));
            this.connection.RegisterCallback<GameStarted>(new GruntCallback<GameStarted>(this.OnGameStarted));
            this.connection.RegisterCallback<GameStatus>(new GruntCallback<GameStatus>(this.OnGameStatus));
            this.connection.RegisterCallback<DrawStatus>(new GruntCallback<DrawStatus>(this.OnDrawStatus));
            this.connection.RegisterCallback<Ping>(new GruntCallback<Ping>(this.OnPong));
            this.connection.RegisterCallback<Broadcast>(new GruntCallback<Broadcast>(this.OnBroadcast));
            this.connection.ConnectionClosed += new GruntConnection.ConnectionEvent(this.connection_ConnectionClosed);
        }

        private void connection_ConnectionClosed(GruntConnection connection)
        {
            System.Console.WriteLine("Connection closed");
        }

        private void SendJoinLobby(int lobbyID)
        {
            JoinLobby message = new JoinLobby();
            message.LobbyId = lobbyID;
            this.connection.Send(message);
        }

        private void SendLeaveLobby()
        {
            LeaveLobby message = new LeaveLobby();
            this.connection.Send(message);
        }

        private void SendCreateLobby(GameType type, int moveSpeed, int moveDelay, bool rated, int teamConfig)
        {
            CreateNewLobby message = new CreateNewLobby();
            LobbySettings settings = new LobbySettings();
            settings.GameType = type;
            settings.MillisecondsForWait = moveDelay;
            settings.MillisecondsPerSquare = moveSpeed;
            settings.IsRated = rated;
            settings.TeamConfiguration = teamConfig;
            message.Settings = settings;
            this.connection.Send(message);
        }

        private void SendSitRequest(int seatNum)
        {
            PlayerSitRequest sitReq = new PlayerSitRequest();
            sitReq.Position = seatNum;
            this.connection.Send(sitReq);
        }

        private void SendStandRequest()
        {
            PlayerStandRequest standReq = new PlayerStandRequest();
            this.connection.Send(standReq);
        }

        private void SendReadyRequest(bool ready)
        {
            PlayerReady message = new PlayerReady();
            message.IsReady = ready;
            this.connection.Send(message);
        }

        private void SendChatMessage(string message)
        {
            SendChatMessage message2 = new SendChatMessage();
            message2.Destination = ChatDestinationEnum.LOBBY;
            message2.Message = message;
            this.connection.Send(message2);
        }

        public void SendPieceMove(int pieceId, int x, int y)
        {
            PieceMoveRequest request = new PieceMoveRequest();
            request.PieceId = pieceId;
            request.TargetX = x;
            request.TargetY = y;
            this.connection.Send(request);
        }

        public void DropPiece(PieceType type, int x, int y)
        {
            PlaceRequest placeRequest = new PlaceRequest();
            placeRequest.TargetX = x;
            placeRequest.TargetY = y;
            placeRequest.Type = (int)type;
            this.connection.Send(placeRequest);
        }

        public void SendOfferDraw()
        {
            DrawRequest drawMessage = new DrawRequest();
            drawMessage.DrawOffered = true;
            this.connection.Send(drawMessage);
        }

        public void SendResign()
        {
            ResignRequest resignMessage = new ResignRequest();
            this.connection.Send(resignMessage);
        }

        private void OnConnectionAuthenticationDemand(GruntConnection conn, ConnectionAuthenticationDemand message)
        {            
            System.Console.WriteLine("Authentication demand");
            byte[] salt = message.Salt;
            byte[] random = message.RandomBytes;

            byte[] saltedPasswordHash = this.BuildSaltedPasswordHash(salt, this.doHash(this.password));
            string password = this.BuildFinalPasswordHashString(random, saltedPasswordHash);
            this.SendAuthentication(password);
        }

        public string BuildFinalPasswordHashString(byte[] randomBytes, byte[] saltedPasswordHash)
        {
            byte[] array = new byte[randomBytes.Length + saltedPasswordHash.Length];
            randomBytes.CopyTo(array, 0);
            saltedPasswordHash.CopyTo(array, randomBytes.Length);
            return ToHex(GetHash(array));
        }

        public byte[] BuildSaltedPasswordHash(byte[] salt, byte[] hashedPassword)
        {
            byte[] array = new byte[salt.Length + hashedPassword.Length];
            salt.CopyTo(array, 0);
            hashedPassword.CopyTo(array, salt.Length);
            return this.GetHash(array);
        }

        public static string ToHex(byte[] array)
        {
            StringBuilder builder = new StringBuilder();
            foreach (byte num in array)
            {
                builder.AppendFormat("{0:X2}", num);
            }
            return builder.ToString();
        }

        public byte[] GetHash(byte[] data)
        {
            SHA256Managed managed = new SHA256Managed();
            return managed.ComputeHash(data);
        }

        public void SendAuthentication(string password)
        {
            ConnectionAuthentication message = new ConnectionAuthentication();
            message.Password = password;
            this.connection.Send(message);
            System.Console.WriteLine("Sent authentication: " + password);
        }
    }
}
